package com.fiberhome.app.network.download;

import android.content.Context;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.ObservableSource;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * based on RXAndroid + OKHttp3.0 Created by Deed on 2018/1/11.
 */
public final class DownloadManager {

	private static final String TAG = "com.fiberhome.app.network.download.DownloadManager";
	private Map<String, Call> downCalls;
	private Map<String, DownloadObserver> observers;
	private OkHttpClient mClient;
	private Context context;
	private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<DownloadManager>();// 线程安全

	/**
	 * @return DownloadManager
	 */
	public static DownloadManager getInstance() {
		for (;;) {
			DownloadManager current = INSTANCE.get();
			if (current != null) {
				return current;
			}
			current = new DownloadManager();
			if (INSTANCE.compareAndSet(null, current)) {
				return current;
			}
		}
	}

	private DownloadManager() {
		downCalls = new HashMap<String, Call>();
		observers = new HashMap<String, DownloadObserver>();
		mClient = new OkHttpClient.Builder().build();
	}

	/**
	 * @param context
	 *            Context
	 */
	public void init(@NonNull Context context) {
		this.context = context.getApplicationContext();
	}

	/**
	 * @param url
	 *            String
	 * @param listener
	 *            DownloadListener
	 */
	public void download(String url, DownloadListener listener) {
		if (context == null) {
			Log.w(TAG, "please call init(Context) first");
			return;
		}
		download(url, context.getFilesDir().getPath(), listener);

	}

	/**
	 * @param url
	 *            String
	 * @param destDir
	 *            String
	 * @param listener
	 *            DownloadListener
	 */
	public void download(String url, final String destDir,
			DownloadListener listener) {
		Log.i(TAG, "download url :" + url + " to Destination Directory:" + destDir);
		Observable observable = Observable
				.just(url)
				.filter(new Predicate<String>() {
					@Override
					public boolean test(@NonNull String s) throws Exception {
						return !downCalls.containsKey(s);
					}
				})
				.flatMap(
						new Function<String, ObservableSource<DownloadBean>>() {
							@Override
							public ObservableSource<DownloadBean> apply(
									@NonNull String s) throws Exception {
								return Observable.just(createDownloadBean(s));
							}
						})
				.map(new Function<DownloadBean, DownloadBean>() {
					@Override
					public DownloadBean apply(@NonNull DownloadBean o)
							throws Exception {
						return getRealFileName(o, destDir);
					}
				})
				.flatMap(new Function<DownloadBean, ObservableSource<?>>() {
					@Override
					public ObservableSource<?> apply(
							@NonNull DownloadBean downloadBean)
							throws Exception {
						return Observable.create(new DownloadSubscribe(
								downloadBean, destDir));
					}
				}).observeOn(AndroidSchedulers.mainThread())
				.subscribeOn(Schedulers.io());
		DownloadObserver o = observers.get(url);
		if (o != null && !o.isDisposed()) {
			o.downloadListener = listener;
			Log.i(TAG, "reuse observer for download");
		} else {
			Log.i(TAG, "use new observer for download");
			observable.subscribe(new DownloadObserver(listener, url));
		}

	}

	/**
	 * 
	 * @param url
	 *            String
	 * @return DownloadBean
	 */
	public DownloadBean contains(String url) {
		DownloadObserver o = observers.get(url);
		if (o != null) {
			return o.downloadBean;
		}
		return null;
	}

	/**
	 * 
	 * @param url
	 *            String
	 */
	public void cancel(String url) {
		Call call = downCalls.get(url);
		if (call != null) {
			call.cancel();
		}
		downCalls.remove(url);
		observers.remove(url);
	}

	/**
	 * create DownloadBean
	 * 
	 * @param url
	 *            String
	 * @return DownloadBean
	 */
	private DownloadBean createDownloadBean(String url) throws IOException {
		DownloadBean downloadBean = new DownloadBean(url);
		long contentLength = getContentLength(url);
		downloadBean.totalSize = contentLength;
		String fileName = url.substring(url.lastIndexOf("/"));
		downloadBean.fileName = fileName;
		return downloadBean;
	}

	/**
	 * get download length
	 * 
	 * @param downloadUrl
	 *            String
	 * @return long
	 */
	private long getContentLength(String downloadUrl) throws IOException {
		Request request = new Request.Builder().url(downloadUrl).build();
		Response response = mClient.newCall(request).execute();
		if (response != null && response.isSuccessful()) {
			long contentLength = response.body().contentLength();
			response.close();
			return contentLength == 0 ? DownloadBean.SIZE_ERROR : contentLength;
		}
		return DownloadBean.SIZE_ERROR;
	}

	private DownloadBean getRealFileName(DownloadBean downloadBean,
			String rootDir) {
		String fileName = downloadBean.fileName;
		long downloadLength = 0, contentLength = downloadBean.totalSize;
		File file = new File(rootDir, fileName);
		if (file.exists()) {
			downloadLength = file.length();
		}
		int i = 1;
		while (downloadLength >= contentLength) {
			int dotIndex = fileName.lastIndexOf(".");
			String fileNameOther;
			if (dotIndex == -1) {
				fileNameOther = fileName + "(" + i + ")";
			} else {
				fileNameOther = fileName.substring(0, dotIndex) + "(" + i + ")"
						+ fileName.substring(dotIndex);
			}
			File newFile = new File(rootDir, fileNameOther);
			file = newFile;
			downloadLength = newFile.length();
			i++;
		}
		downloadBean.downloadedSize = downloadLength;
		downloadBean.fileName = file.getName();
		downloadBean.filePath = file;
		return downloadBean;
	}

	private class DownloadObserver implements Observer<DownloadBean> {
		Disposable d;
		DownloadBean downloadBean;
		private DownloadListener downloadListener;
		private String url;

		private DownloadObserver(DownloadListener downloadListener, String url) {
			this.downloadListener = downloadListener;
			this.url = url;
		}

		@Override
		public void onSubscribe(@NonNull Disposable d) {
			this.d = d;
			observers.put(url, this);
		}

		@Override
		public void onNext(@NonNull DownloadBean downloadBean) {
			this.downloadBean = downloadBean;
			if (this.downloadListener != null) {
				downloadListener.onUpdate(downloadBean);
			}
		}

		@Override
		public void onError(@NonNull Throwable e) {
			if (this.downloadListener != null) {
				this.downloadListener.onError(e);
			}
		}
		
		public boolean isDisposed() {
			if (d != null) {
				return d.isDisposed();
			}
			return true;
		}

		@Override
		public void onComplete() {
			observers.remove(url);
			downCalls.remove(url);
			if (this.downloadListener != null) {
				this.downloadListener.onComplete(downloadBean);
			}
		}
	}

	private class DownloadSubscribe implements
			ObservableOnSubscribe<DownloadBean> {
		private DownloadBean downloadInfo;
		private String destDir;

		/**
		 * 
		 * @param downloadInfo
		 *            DownloadBean
		 * @param destDir
		 *            String
		 */
		DownloadSubscribe(DownloadBean downloadInfo, String destDir) {
			this.downloadInfo = downloadInfo;
			this.destDir = destDir;
		}

		@Override
		public void subscribe(ObservableEmitter<DownloadBean> e)
				throws Exception {
			String url = downloadInfo.url;
			long downloadLength = downloadInfo.downloadedSize;
			long contentLength = downloadInfo.totalSize;
			e.onNext(downloadInfo);

			Request request = new Request.Builder()
					.addHeader("RANGE",
							"bytes=" + downloadLength + "-" + contentLength)
					.url(url).build();
			Call call = mClient.newCall(request);
			downCalls.put(url, call);
			Response response = call.execute();

			File file = new File(destDir, downloadInfo.fileName);
			InputStream is = null;
			FileOutputStream fileOutputStream = null;
			try {
				is = response.body().byteStream();
				fileOutputStream = new FileOutputStream(file, true);
				byte[] buffer = new byte[2048];
				int len;
				while ((len = is.read(buffer)) != -1) {
					fileOutputStream.write(buffer, 0, len);
					downloadLength += len;
					downloadInfo.downloadedSize = downloadLength;
					e.onNext(downloadInfo);
					Log.i(TAG, "Download progress of " + downloadInfo.fileName + " : " 
					+ downloadInfo.downloadedSize + "/" + downloadInfo.totalSize);
				}
				fileOutputStream.flush();
				downCalls.remove(url);
			} finally {
				if (is != null) {
					is.close();
				}
				if (fileOutputStream != null) {
					fileOutputStream.close();
				}

			}
			e.onComplete();
		}
	}
}
